import random
import numpy as np
import matplotlib.pyplot as plt
def make_empty_grid(w, h):
grid = np.ones((h, w), dtype=np.uint8) # 1 = wall, 0 = passage
return grid
def carve_path(grid, x, y):
grid[y, x] = 0
def neighbors4(x, y, w, h, step=2):
nb = []
if x - step >= 1: nb.append((x-step, y))
if x + step < w-1: nb.append((x+step, y))
if y - step >= 1: nb.append((x, y-step))
if y + step < h-1: nb.append((x, y+step))
return nb
def dfs_maze(width, height, seed=None):
if seed is not None:
random.seed(seed)
w,h = width, height
grid = make_empty_grid(w,h)
sx = random.randrange(1, w, 2)
sy = random.randrange(1, h, 2)
carve_path(grid, sx, sy)
stack = [(sx, sy)]
while stack:
x,y = stack[-1]
nbs = neighbors4(x,y,w,h,2)
unvisited = [ (nx,ny) for (nx,ny) in nbs if grid[ny,nx]==1 ]
if unvisited:
nx,ny = random.choice(unvisited)
wx,wy = (x+nx)//2, (y+ny)//2
carve_path(grid, wx, wy)
carve_path(grid, nx, ny)
stack.append((nx,ny))
else:
stack.pop()
return grid
def prim_maze(width, height, seed=None):
if seed is not None:
random.seed(seed+1)
w,h = width, height
grid = make_empty_grid(w,h)
sx = random.randrange(1, w, 2)
sy = random.randrange(1, h, 2)
carve_path(grid, sx, sy)
walls = []
for nx,ny in neighbors4(sx,sy,w,h,2):
walls.append(((sx,sy),(nx,ny)))
while walls:
a,b = random.choice(walls)
walls.remove((a,b))
x1,y1 = a; x2,y2 = b
if grid[y2,x2] == 1:
wx,wy = (x1+x2)//2, (y1+y2)//2
carve_path(grid, wx, wy)
carve_path(grid, x2, y2)
for nx,ny in neighbors4(x2,y2,w,h,2):
if grid[ny,nx]==1:
walls.append(((x2,y2),(nx,ny)))
return grid
def find(parents, a):
while parents[a]!=a:
parents[a]=parents[parents[a]]
a=parents[a]
return a
def union(parents, ranks, a, b):
ra, rb = find(parents,a), find(parents,b)
if ra==rb: return False
if ranks[ra] < ranks[rb]:
parents[ra]=rb
else:
parents[rb]=ra
if ranks[ra]==ranks[rb]:
ranks[ra]+=1
return True
def kruskal_maze(width, height, seed=None):
if seed is not None:
random.seed(seed+2)
w,h = width, height
grid = make_empty_grid(w,h)
cells = []
index = {}
idx = 0
for y in range(1,h,2):
for x in range(1,w,2):
cells.append((x,y))
index[(x,y)] = idx
carve_path(grid, x, y)
idx += 1
walls = []
for (x,y) in cells:
if x+2 < w:
walls.append(((x,y),(x+2,y)))
if y+2 < h:
walls.append(((x,y),(x,y+2)))
random.shuffle(walls)
parents = list(range(len(cells)))
ranks = [0]*len(cells)
for a,b in walls:
ia = index[a]; ib = index[b]
if find(parents, ia) != find(parents, ib):
union(parents, ranks, ia, ib)
wx,wy = (a[0]+b[0])//2, (a[1]+b[1])//2
carve_path(grid, wx, wy)
return grid
def recursive_division(width, height, seed=None):
if seed is not None:
random.seed(seed+3)
w,h = width, height
grid = np.zeros((h,w), dtype=np.uint8) # 0 = passage
# outer walls
grid[0,:] = 1
grid[h-1,:] = 1
grid[:,0] = 1
grid[:,w-1] = 1
def choose_even(a, b):
return [i for i in range(a, b+1) if i%2==0]
def choose_odd(a, b):
return [i for i in range(a, b+1) if i%2==1]
def divide(x1,y1,x2,y2):
dx = x2 - x1
dy = y2 - y1
if dx < 2 or dy < 2:
return
horizontal = True if dx < dy else False
if dx == dy:
horizontal = random.choice([True, False])
if horizontal:
even_rows = choose_even(y1+1, y2-1)
if not even_rows:
return
wy = random.choice(even_rows)
# draw wall
for x in range(x1, x2+1):
grid[wy, x] = 1
# make hole at odd column
odd_cols = choose_odd(x1, x2)
if not odd_cols:
return
hole = random.choice(odd_cols)
grid[wy, hole] = 0
divide(x1, y1, x2, wy-1)
divide(x1, wy+1, x2, y2)
else:
even_cols = choose_even(x1+1, x2-1)
if not even_cols:
return
wx = random.choice(even_cols)
for y in range(y1, y2+1):
grid[y, wx] = 1
odd_rows = choose_odd(y1, y2)
if not odd_rows:
return
hole = random.choice(odd_rows)
grid[hole, wx] = 0
divide(x1, y1, wx-1, y2)
divide(wx+1, y1, x2, y2)
divide(1,1,w-2,h-2)
return grid
def show_maze(grid, title):
plt.figure(figsize=(5,5))
plt.imshow(grid)
plt.title(title)
plt.axis('off')
plt.show()
W, H = 51, 51
dfs = dfs_maze(W,H, seed=42)
prims = prim_maze(W,H, seed=42)
kruskal = kruskal_maze(W,H, seed=42)
recdiv = recursive_division(W,H, seed=42)
show_maze(dfs, "DFS (Recursive Backtracker)")
show_maze(prims, "Randomized Prim's")
show_maze(kruskal, "Kruskal's")
show_maze(recdiv, "Recursive Division")
W2,H2 = 21,21
sample = dfs_maze(W2,H2, seed=7)
ascii_map = "\n".join("".join('█' if sample[y,x]==1 else ' ' for x in range(W2)) for y in range(H2))
print("\nASCII preview (21x21, █ = wall):\n")
print(ascii_map)
STDOUT/STDERR
\nASCII preview (21x21, █ = wall):\n
█████████████████████
█ █
███████████████ ███ █
█ █ █ █ █
█ █████ █ ███ ███ █ █
█ █ █ █ █ █ █ █
█ █ █ ███ █ ███ █ ███
█ █ █ █ █ █ █ █
█ █████████ █ █ ███ █
█ █ █ █ █ █
█ █ █ █ █████ █████ █
█ █ █ █ █ █
█████████ █ ███ █ █ █
█ █ █ █ █ █ █
█ ███ █████ █ ███ █ █
█ █ █ █ █ █ █
█ █ ███ █ █████ █ █ █
█ █ █ █ █ █ █
█ █ █ ███ █████████ █
█ █ █ █
█████████████████████
DFS / Recursive Backtracker:從隨機起點做深度優先挖通,結果通常是長走廊、少環路(典型「拉長路徑」風格)。
Randomized Prim's:從一個起始細胞把相鄰牆當 frontier 隨機選擇,挖通後加入新的 frontier。較均勻、迴路較少但比 DFS 稍微分散。
Kruskal's(用聯集-找集合):把每個細胞視為節點,隨機考慮拆除牆(邊),若拆掉不會形成迴圈就拆,最終生成一個隨機生成的最小生成樹(maze)。風格與 Prim 類似但隨機性來源不同。
Recursive Division(遞迴分割):從空間畫牆分割子區域,並在牆上留通口(門)。通常產生較多直線長牆與區塊感。
完整可執行的 Python 程式(我在 notebook 執行過):
四個演算法實作:dfs_maze, prim_maze, kruskal_maze, recursive_division
每個演算法輸出為 2D numpy 陣列(0 = 通路,1 = 牆)
使用 matplotlib 分別顯示四張圖(每張圖單獨一個圖表)
還生成一個 21×21 的 ASCII 小範例做預覽(在程式輸出底部)
。